package crmdna.member;

import static crmdna.common.AssertUtils.ensureEqual;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;
import static crmdna.member.MemberSaver.memberSaver;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.DateUtils;
import crmdna.common.Utils;
import crmdna.common.contact.Contact;
import crmdna.common.contact.ContactProp;
import crmdna.member.MemberEntity.MemberFactory;
import crmdna.program.Program;
import crmdna.program.ProgramProp;
import crmdna.programtype.ProgramType;
import crmdna.sequence.Sequence.SequenceType;
import crmdna.user.User;
import crmdna.user.UserCore;

public class Member {

	public static MemberProp create(String client, long groupId,
			ContactProp contact, boolean allowDuplicateEmail, String login) {

		Client.ensureValid(client);
		User.ensureValidUser(client, login);

		ensureNotNull(contact);

		Contact.ensureEmailOrPhoneNumberValid(contact);

		if (!allowDuplicateEmail) {
			// if email is specified - it should be unique
			if (null != contact.email) {
				contact.email = contact.email.toLowerCase();
				ensureEmailNotPresentInDB(client, contact.email, login);
				safeAddToMemcache(client, contact.email);
			}
		}

		MemberEntity memberEntity = MemberFactory.create(client, contact,
				groupId);

		MemberSaver.get(client, memberEntity).populateDependantsAndSave();

		return memberEntity.toProp();
	}

	static void safeAddToMemcache(String client, String email) {
		// automatically add key to memcache, if already found throw exception

		String key = getUniqueKey(client, email.toLowerCase());
		NamespaceManager.set(client);
		long val = MemcacheServiceFactory.getMemcacheService().increment(key,
				(long) 1, (long) 0);
		// note: call to increment is atomic
		if (val != 1)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a member with email [" + email + "]");
	}

	private static String getUniqueKey(String client, String name) {
		return client + "_" + SequenceType.MEMBER + "_" + name;
	}

	static void ensureEmailNotPresentInDB(String client, String email,
			String login) {

		ensureNotNull(email);
		email = email.toLowerCase();

		MemberQueryCondition condition = new MemberQueryCondition(client, 10000)
				.email(email);

		List<Key<MemberEntity>> keys = MemberLoader.queryKeys(condition, login);

		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a member with email [" + email + "]");
	}

	public static MemberProp updateContactDetails(final String client,
			final long memberId, final ContactProp contact, String login) {

		Client.ensureValid(client);
		User.ensureValidUser(client, login);

		MemberEntity memberEntity = MemberLoader.safeGet(client, memberId,
				login);

		// if email is different it should not exist in a different entity
		if ((memberEntity.email != null)
				&& (contact.email != null)
				&& (!memberEntity.email.toLowerCase().equals(
						contact.email.toLowerCase()))) {

			contact.email = contact.email.toLowerCase();
			ensureEmailNotPresentInDB(client, contact.email, login);
			safeAddToMemcache(client, contact.email);
		}

		memberSaver(client, Utils.getList(memberEntity))
				.setContactsSameSizeList(Utils.getList(contact))
				.populateDependantsAndSave();

		return memberEntity.toProp();
	}

	public static MemberProp addOrDeleteGroup(String client, long memberId,
			long groupId, boolean add, String login) {

		MemberEntity memberEntity = MemberLoader.safeGet(client, memberId,
				login);

		memberSaver(client, Utils.getList(memberEntity)).addGroupToAll(groupId)
				.populateDependantsAndSave();

		return memberEntity.toProp();
	}

	public static MemberProp addOrDeleteProgram(String client, long memberId,
			long programId, boolean add, String login) {

		Client.ensureValid(client);
		UserCore.ensureValidUser(client, login);

		MemberEntity memberEntity = MemberLoader.safeGet(client, memberId,
				login);
		Program.safeGet(client, programId);

		memberSaver(client, Utils.getList(memberEntity)).addProgramToAll(
				programId).populateDependantsAndSave();

		return memberEntity.toProp();
	}

	public static Long getMatchingMemberId(String client, String email,
			String firstName, String lastName) {
		List<ContactProp> contactDetailProps = new ArrayList<>();

		ContactProp c = new ContactProp();
		c.firstName = firstName;
		c.lastName = lastName;
		c.email = email;

		contactDetailProps.add(c);

		return getMatchingMemberIds(client, contactDetailProps).get(0);
	}

	public static List<Long> getMatchingMemberIds(String client,
			List<ContactProp> contactDetailProps) {

		Client.ensureValid(client);

		if (contactDetailProps.size() == 0)
			return new ArrayList<>();

		for (ContactProp contactDetailProp : contactDetailProps) {
			if (contactDetailProp.email != null)
				contactDetailProp.email = contactDetailProp.email.toLowerCase();
		}

		Map<String, Set<Integer>> emailMap = new HashMap<>();
		Map<String, Set<Integer>> mobileMap = new HashMap<>();

		for (int index = 0; index < contactDetailProps.size(); index++) {
			ContactProp c = contactDetailProps.get(index);
			if (c.email != null) {
				if (!emailMap.containsKey(c.email))
					emailMap.put(c.email, new HashSet<Integer>());

				Set<Integer> existing = emailMap.get(c.email);
				existing.add(index);
			} else if (c.mobilePhone != null) {
				if (!mobileMap.containsKey(c.mobilePhone))
					mobileMap.put(c.mobilePhone, new HashSet<Integer>());

				Set<Integer> existing = mobileMap.get(c.mobilePhone);
				existing.add(index);
			}
		}

		List<Long> memberIds = new ArrayList<>();
		for (int i = 0; i < contactDetailProps.size(); i++) {
			memberIds.add(null);
		}

		List<MemberEntity> emailMatches = new ArrayList<>();
		if (emailMap.size() != 0)
			emailMatches = ofy(client).load().type(MemberEntity.class)
					.filter("email in", emailMap.keySet()).list();

		List<MemberEntity> mobileMatches = new ArrayList<>();
		if (mobileMap.size() != 0) {
			mobileMatches = ofy(client).load().type(MemberEntity.class)
					.filter("mobilePhone in", mobileMap.keySet()).list();
		}

		for (MemberEntity me : emailMatches) {
			MemberProp mp = me.toProp();
			String memberName = mp.contact.getName();

			for (Integer index : emailMap.get(mp.contact.email)) {
				String particantName = contactDetailProps.get(index).getName();

				if (Utils.closeEnough(memberName, particantName))
					memberIds.set(index, mp.memberId);
			}
		}

		for (MemberEntity me : mobileMatches) {
			MemberProp mp = me.toProp();
			String memberName = mp.contact.getName();

			for (Integer index : mobileMap.get(mp.contact.mobilePhone)) {
				if (memberIds.get(index) == null) {
					String particantName = contactDetailProps.get(index)
							.getName();

					if (Utils.closeEnough(memberName, particantName))
						memberIds.set(index, mp.memberId);
				}
			}
		}

		return memberIds;
	}

	public static List<MemberEntity> getMatchingMembersSameSizeList(
			String client, List<ContactProp> contacts, String login) {

		// Returns a list same size as contacts
		// list element contains null when there is no match, else matching
		// member id

		Client.ensureValid(client);

		ensureNotNull(contacts, "contacts is null");

		Set<String> firstName3Chars = new HashSet<>();
		for (ContactProp c : contacts) {
			if ((c == null) || (c.firstName == null)
					|| (c.firstName.length() < 3))
				continue;

			String firstName3Char = c.firstName.substring(0, 3).toLowerCase();

			firstName3Chars.add(firstName3Char);
		}

		MemberQueryCondition qc = new MemberQueryCondition(client, 10000)
				.firstName3Char(firstName3Chars);

		List<Key<MemberEntity>> matchingMemberKeys = MemberLoader.queryKeys(qc,
				login);

		Iterable<MemberEntity> entities = ofy(client).load()
				.keys(matchingMemberKeys).values();

		Map<String, Set<MemberEntity>> firstName3CharVsEntity = new HashMap<>(); // multimap

		for (MemberEntity entity : entities) {

			String firstName3Char = entity.firstName3Char;

			if (!firstName3CharVsEntity.containsKey(firstName3Char)) {
				firstName3CharVsEntity.put(firstName3Char,
						new HashSet<MemberEntity>());
			}

			firstName3CharVsEntity.get(firstName3Char).add(entity);
		}

		List<MemberEntity> matchingEntities = new ArrayList<>(contacts.size());
		for (ContactProp c : contacts) {
			if ((c == null) || (c.firstName == null)
					|| (c.firstName.length() < 3)) {
				matchingEntities.add(null);
				continue;
			}

			String firstName3Char = c.firstName.substring(0, 3).toLowerCase();

			if (!firstName3CharVsEntity.containsKey(firstName3Char)) {
				// no matching member
				matchingEntities.add(null);
				continue;
			}

			Set<MemberEntity> set = firstName3CharVsEntity.get(firstName3Char);

			MemberEntity entity = findMatchingMember(c, set);
			matchingEntities.add(entity);
		}

		ensureEqual(contacts.size(), matchingEntities.size());

		return matchingEntities;
	}

	public static MemberEntity findMatchingMember(ContactProp contactProp,
			Set<MemberEntity> memberEntities) {

		ensureNotNull(contactProp, "contactProp cannot be null");
		ensureNotNull(memberEntities, "memberEntities cannot be null");

		for (MemberEntity memberEntity : memberEntities) {
			if (Contact.isMatching(contactProp, memberEntity.toProp().contact))
				return memberEntity;
		}

		return null;
	}

	static boolean populateContactDetails(MemberEntity memberEntity,
			ContactProp contact) {

		// TODO: use a smarter home address comparison function

		DateUtils.ensureFormatYYYYMMDD(contact.asOfyyyymmdd);

		if (memberEntity.asOfYYYYMMDD > contact.asOfyyyymmdd)
			return false;

		boolean changed = false;

		if (contact.email != null) {
			Utils.ensureValidEmail(contact.email);

			if (Utils.isDifferentCaseInsensitive(memberEntity.email,
					contact.email)) {
				memberEntity.email = contact.email.toLowerCase();
				changed = true;
			}
		}

		if (contact.firstName != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.firstName,
					contact.firstName)) {
				memberEntity.firstName = contact.firstName;
				changed = true;
			}
		}

		if (contact.lastName != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.lastName,
					contact.lastName)) {
				memberEntity.lastName = contact.lastName;
				changed = true;
			}
		}

		if (contact.homeAddress.address != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.homeAddress,
					contact.homeAddress.address)) {
				memberEntity.homeAddress = contact.homeAddress.address;
				changed = true;
			}
		}

		if (contact.homeAddress.city != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.homeCity,
					contact.homeAddress.city)) {
				memberEntity.homeCity = contact.homeAddress.city;
				changed = true;
			}
		}

		if (contact.homeAddress.state != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.homeState,
					contact.homeAddress.state)) {
				memberEntity.homeState = contact.homeAddress.state;
				changed = true;
			}
		}

		if (contact.homeAddress.pincode != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.homePincode,
					contact.homeAddress.pincode)) {
				memberEntity.homePincode = contact.homeAddress.pincode;
				changed = true;
			}
		}

		if (contact.homeAddress.country != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.homeCountry,
					contact.homeAddress.country)) {
				memberEntity.homeCountry = contact.homeAddress.country;
				changed = true;
			}
		}

		if (contact.officeAddress.address != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.officeAddress,
					contact.officeAddress.address)) {
				memberEntity.officeAddress = contact.officeAddress.address;
				changed = true;
			}
		}

		if (contact.officeAddress.pincode != null) {
			if (Utils.isDifferentCaseInsensitive(memberEntity.officePincode,
					contact.officeAddress.pincode)) {
				memberEntity.officePincode = contact.officeAddress.pincode;
				changed = true;
			}
		}

		if (contact.homePhone != null) {
			Utils.ensureValidPhoneNumber(contact.homePhone);
			if (Utils.isDifferentCaseInsensitive(memberEntity.homePhone,
					contact.homePhone)) {
				memberEntity.homePhone = contact.homePhone;
				changed = true;
			}
		}

		if (contact.officePhone != null) {
			Utils.ensureValidPhoneNumber(contact.officePhone);
			if (Utils.isDifferentCaseInsensitive(memberEntity.officePhone,
					contact.officePhone)) {
				memberEntity.officePhone = contact.officePhone;
				changed = true;
			}
		}

		if (contact.mobilePhone != null) {
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
			if (Utils.isDifferentCaseInsensitive(memberEntity.mobilePhone,
					contact.mobilePhone)) {
				memberEntity.mobilePhone = contact.mobilePhone;
				changed = true;
			}
		}

		if (changed)
			memberEntity.asOfYYYYMMDD = contact.asOfyyyymmdd;

		return changed;
	}

	static void populateDependantFields(String client,
			List<MemberEntity> memberEntities) {

		ensureNotNull(memberEntities, "memberEntities cannot be null");

		for (MemberEntity memberEntity : memberEntities) {
			ensureNotNull(memberEntity,
					"individual memberEntity cannot be null");
			populateDependantFieldsWithoutDBQuery(memberEntity);
		}

		// fields that involve database queries
		List<Long> programIds = new ArrayList<>();
		for (MemberEntity memberEntity : memberEntities) {
			programIds.addAll(memberEntity.programIds);
		}

		Map<Long, ProgramProp> programProps = Program.get(client, programIds);

		for (MemberEntity memberEntity : memberEntities) {
			memberEntity.programTypeIds.clear();
			memberEntity.practiceIds.clear();

			for (long programId : memberEntity.programIds) {
				if (programProps.containsKey(programId)) {
					ProgramProp programProp = programProps.get(programId);

					memberEntity.programTypeIds
							.add(programProp.programTypeProp.programTypeId);
				}
			}

			List<UnverifiedProgramProp> uvprops = UnverifiedProgram
					.getUnverifiedProgramPropsFromJson(memberEntity.listUnverfiedProgramPropJson);

			if (uvprops != null) {
				for (UnverifiedProgramProp unverifiedProgramProp : uvprops) {
					memberEntity.programTypeIds
							.add(unverifiedProgramProp.programTypeId);
				}
			}

			memberEntity.practiceIds = ProgramType.getPracticeIds(client,
					memberEntity.programTypeIds);
		}
	}

	static MemberEntity populateDependantFieldsWithoutDBQuery(
			MemberEntity memberEntity) {
		// nameFirstChar
		if (memberEntity.firstName != null) {
			memberEntity.name = memberEntity.firstName.toLowerCase();
		} else if (memberEntity.email != null)
			memberEntity.name = memberEntity.email.toLowerCase();
		else if (memberEntity.mobilePhone != null)
			memberEntity.name = memberEntity.mobilePhone.toLowerCase();
		else if (memberEntity.homePhone != null)
			memberEntity.name = memberEntity.homePhone.toLowerCase();
		else if (memberEntity.officePhone != null)
			memberEntity.name = memberEntity.officePhone.toLowerCase();
		else {
			// should never come here as either email or a valid phone number
			// should be specified
			throw new APIException(
					Status.ERROR_RESOURCE_INCORRECT,
					"Either email or a valid phone number should be specified when adding or updating member");
		}
		memberEntity.nameFirstChar = memberEntity.name.substring(0, 1);

		if ((memberEntity.firstName != null)
				&& (memberEntity.firstName.length() > 2))
			memberEntity.firstName3Char = memberEntity.firstName
					.substring(0, 3).toLowerCase();

		memberEntity.qsTags = Utils.getQSTags(memberEntity.email,
				memberEntity.firstName, memberEntity.lastName,
				memberEntity.homePhone, memberEntity.officePhone,
				memberEntity.mobilePhone);

		// phone numbers
		HashSet<String> phoneNumbers = new HashSet<>();
		if (memberEntity.mobilePhone != null)
			phoneNumbers.add(memberEntity.mobilePhone);
		if (memberEntity.officePhone != null)
			phoneNumbers.add(memberEntity.officePhone);
		if (memberEntity.homePhone != null)
			phoneNumbers.add(memberEntity.homePhone);

		memberEntity.phoneNumbers = phoneNumbers;

		return memberEntity;
	}

	// This should be removed and replaced by populateContactDetailsIfNew
	static MemberEntity populateContactDetails_to_be_removed(
			MemberEntity memberEntity, ContactProp contact) {
		if (contact.email != null) {
			Utils.ensureValidEmail(contact.email);
			memberEntity.email = contact.email.toLowerCase();
		}

		if (contact.firstName != null)
			memberEntity.firstName = contact.firstName;

		if (contact.lastName != null)
			memberEntity.lastName = contact.lastName;

		if (contact.homeAddress != null)
			memberEntity.homeAddress = contact.homeAddress.address;

		if (contact.officeAddress != null)
			memberEntity.officeAddress = contact.officeAddress.address;

		if (contact.homePhone != null) {
			Utils.ensureValidPhoneNumber(contact.homePhone);
			memberEntity.homePhone = contact.homePhone;
		}

		if (contact.officePhone != null) {
			Utils.ensureValidPhoneNumber(contact.officePhone);
			memberEntity.officePhone = contact.officePhone;
		}

		if (contact.mobilePhone != null) {
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
			memberEntity.mobilePhone = contact.mobilePhone;
		}

		return memberEntity;
	}
}